/**
 * Software License, Version 1.0 Copyright 2003 The Trustees of Indiana
 * University. All rights reserved. Redistribution and use in source and binary
 * forms, with or without modification, are permitted provided that the
 * following conditions are met: 1) All redistributions of source code must
 * retain the above copyright notice, the list of authors in the original source
 * code, this list of conditions and the disclaimer listed in this license; 2)
 * All redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the disclaimer listed in this license in the
 * documentation and/or other materials provided with the distribution; 3) Any
 * documentation included with all redistributions must include the following
 * acknowledgement: "This product includes software developed by the Community
 * Grids Lab. For further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/." Alternatively, this acknowledgement may
 * appear in the software itself, and wherever such third-party acknowledgments
 * normally appear. 4) The name Indiana University or Community Grids Lab or
 * NaradaBrokering, shall not be used to endorse or promote products derived
 * from this software without prior written permission from Indiana University.
 * For written permission, please contact the Advanced Research and Technology
 * Institute ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5)
 * Products derived from this software may not be called NaradaBrokering, nor
 * may Indiana University or Community Grids Lab or NaradaBrokering appear in
 * their name, without prior written permission of ARTI. Indiana University
 * provides no reassurances that the source code provided does not infringe the
 * patent or any other intellectual property rights of any other entity. Indiana
 * University disclaims any liability to any recipient for claims brought by any
 * other entity based on infringement of intellectual property rights or
 * otherwise. LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH
 * NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY
 * GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
/**
 * Software License, Version 1.0 Copyright 2003 The Trustees of Indiana
 * University. All rights reserved. Redistribution and use in source and binary
 * forms, with or without modification, are permitted provided that the
 * following conditions are met: 1) All redistributions of source code must
 * retain the above copyright notice, the list of authors in the original source
 * code, this list of conditions and the disclaimer listed in this license; 2)
 * All redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the disclaimer listed in this license in the
 * documentation and/or other materials provided with the distribution; 3) Any
 * documentation included with all redistributions must include the following
 * acknowledgement: "This product includes software developed by the Community
 * Grids Lab. For further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/." Alternatively, this acknowledgement may
 * appear in the software itself, and wherever such third-party acknowledgments
 * normally appear. 4) The name Indiana University or Community Grids Lab or
 * NaradaBrokering, shall not be used to endorse or promote products derived
 * from this software without prior written permission from Indiana University.
 * For written permission, please contact the Advanced Research and Technology
 * Institute ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5)
 * Products derived from this software may not be called NaradaBrokering, nor
 * may Indiana University or Community Grids Lab or NaradaBrokering appear in
 * their name, without prior written permission of ARTI. Indiana University
 * provides no reassurances that the source code provided does not infringe the
 * patent or any other intellectual property rights of any other entity. Indiana
 * University disclaims any liability to any recipient for claims brought by any
 * other entity based on infringement of intellectual property rights or
 * otherwise. LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH
 * NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY
 * GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.discovery.topics;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Random;
import java.util.TimeZone;
import java.util.Vector;

import org.apache.log4j.Logger;

import cgl.narada.discovery.topics.messages.EncryptedMessagePayload;
import cgl.narada.discovery.topics.messages.SignedTopicAdvertisement;
import cgl.narada.discovery.topics.messages.TDNDiscoveryRequest;
import cgl.narada.discovery.topics.messages.TDNDiscoveryResponse;
import cgl.narada.discovery.topics.messages.TopicAdvertisement;
import cgl.narada.discovery.topics.messages.TopicCreationRequest;
import cgl.narada.event.NBEvent;
import cgl.narada.event.TemplateProfileAndSynopsisTypes;
import cgl.narada.matching.Profile;
import cgl.narada.service.ServiceException;
import cgl.narada.service.ServiceProperties;
import cgl.narada.service.ServicePropertiesFactory;
import cgl.narada.service.client.ClientService;
import cgl.narada.service.client.EventConsumer;
import cgl.narada.service.client.EventProducer;
import cgl.narada.service.client.NBEventListener;
import cgl.narada.service.client.SessionService;
import cgl.narada.service.security.securityprovider.CertificateManager;
import cgl.narada.service.security.securityprovider.CertificateUtil;
import cgl.narada.service.timer.ClockI;
import cgl.narada.service.timer.HRClock;
import cgl.narada.util.SystemInit;
import cgl.narada.util.SystemNotInitializedException;

/**
 * Entity can be used to create topics Created on Apr 11, 2005
 * 
 * @author Harshawardhan Gadgil (hgadgil@grids.ucs.indiana.edu)
 */
public class Entity implements NBEventListener {

   static Logger log = Logger.getLogger("Entity");

   public static final String NEW_TOPIC_PLACEHOLDER = "__PLACEHOLDER__";

   private final String PROVIDER = "CryptixCrypto";

   // Should we start benchmarking... NEEDS recompilation if YES !
   private static final boolean BENCHMARK = false;

   /** ID of this Entity */
   private String EntityID;

   /** Credentials of this Entity */
   private Certificate Credentials;

   private PrivateKey privateKey;

   // Misc
   private EventProducer producer;

   private EventConsumer consumer;

   private String Entity_TOPIC;

   private Hashtable _TDNList;

   private int _maxTDNResponses;

   private String _selectedTDN;

   private Hashtable _myTopicAds;

   private PublicKey rootPublicKey;

   private ServiceProperties serviceProperties;

   private String configFilePath;

   private ClientService clientService;

   private class Synclock {

      public synchronized void lock(long timeout) {
         try {
            wait(timeout);
         } catch (InterruptedException e) {
         }
      }

      public synchronized void unlock() {
         notifyAll();
      }
   }

   Synclock tdnDiscoLock = new Synclock();

   Synclock topicCreationLock = new Synclock();

   // BENCHMARK
   static FileOutputStream fout = null;

   ClockI hrClock = null;

   /**
    * @param clientEntityId -
    *           An int reflecting the id of this client (in this JVM)
    * @param configFile -
    *           Path to ServiceConfiguration.txt file
    * @param id -
    *           Id of this Entity (alias used to identify the key in the
    *           keystore)
    * @param brokerHost -
    *           Host to connect to
    * @param brokerPort -
    *           Port
    * @param brokerConnProtocol -
    *           Protocol to use
    */
   public Entity(int clientEntityId, String configFile, String id,
            Certificate cred, PrivateKey priv, PublicKey rootCAPubKey,
            String brokerHost, String brokerPort, String brokerConnProtocol) {

      configFilePath = configFile;

      ServicePropertiesFactory servicePropertiesFactory = ServicePropertiesFactory
               .getInstance();
      serviceProperties = servicePropertiesFactory
               .getServiceProperties(clientEntityId);

      EntityID = id;

      // Load keys
      Credentials = cred;
      privateKey = priv;

      // Load the ROOT CA's public key for repeated verification purposes
      rootPublicKey = rootCAPubKey;

      // Initializes the Entity by subscribing to various topics.
      Entity_TOPIC = Topics.ENTITY_TOPIC_BASE + "/" + EntityID;

      _TDNList = new Hashtable();
      _myTopicAds = new Hashtable();
      _selectedTDN = null;
      init(clientEntityId, brokerHost, brokerPort, brokerConnProtocol);

      // Create the clock
      String nbHome = null;
      try {
         nbHome = SystemInit.getInitData(SystemInit.SYS_NB_HOME);
      } catch (SystemNotInitializedException e) {
         log.error("", e);
      }

      hrClock = HRClock.getClock(nbHome);
   }

   private void init(int clientEntityId, String brokerHost, String brokerPort,
            String brokerConnProtocol) {
      Properties prop = new Properties();

      // IMP: Might conflict w/ other ppl's entity IDs Maintain constant for
      // now
      int entityId = (new Random()).nextInt();

      prop.put("hostname", brokerHost);
      prop.put("portnum", brokerPort);

      log.info("Connecting using " + brokerConnProtocol + " @ " + brokerHost
               + ":" + brokerPort + " EntityID[" + entityId + "]");

      try {
         clientService = SessionService.getClientService(clientEntityId,
                  configFilePath);
         clientService.initializeBrokerCommunications(prop, brokerConnProtocol);
         consumer = clientService.createEventConsumer(this);

         // Subscribe to my ENTITY_TOPIC
         Profile profile_ENTITY_TOPIC = clientService.createProfile(
                  TemplateProfileAndSynopsisTypes.STRING, Entity_TOPIC);
         consumer.subscribeTo(profile_ENTITY_TOPIC);

         // Subscribe to TOPIC_DISCO for listening and servicing Topic
         // discovery requests
         Profile profile_TOPIC_DISCO = clientService.createProfile(
                  TemplateProfileAndSynopsisTypes.STRING,
                  Topics.TOPIC_DISCOVERY_REQUEST);
         consumer.subscribeTo(profile_TOPIC_DISCO);

         // Create a producer
         producer = clientService.createEventProducer();
         producer.setSuppressRedistributionToSource(true);
      } catch (ServiceException e) {
         log.error("", e);
      }
   }

   /**
    * Broadcasts a TDN discovery request on a predefined topic.
    * 
    * @param timeout
    *           TODO
    * @return TRUE if there is atleast 1 TDN, FALSE otherwise
    */
   public boolean sendTDNDiscoveryRequest(int timeout) {

      long start = hrClock.getTimeMicroseconds();

      _TDNList.clear();
      TDNDiscoveryRequest tdnDiscoReq = new TDNDiscoveryRequest();
      tdnDiscoReq.setEntityId(this.EntityID);
      tdnDiscoReq.setCredentials(Credentials);

      send(Topics.TDN_DISCOVERY, tdnDiscoReq.getBytes());

      // Wait until at least one response comes back OR timeout occurs
      System.out.println(Thread.currentThread().getName() + " WAITING !!");
      tdnDiscoLock.lock(timeout);

      if (BENCHMARK) {
         long stop = hrClock.getTimeMicroseconds();
         long time = stop - start;
         try {
            fout.write(("BENCHMARK # TDNDisco:" + time + "\n").getBytes());
         } catch (IOException e2) {
            log.error("", e2);
         }
      }
      return (_TDNList.size() != 0);
   }

   public void close() {
      try {
         clientService.closeBrokerConnection();
         clientService.terminateServices();
      } catch (ServiceException e) {
         log.error("", e);
      }
   }

   /**
    * Utility function to create a topic. This function waits until a topic
    * creation response was recieved or a timeout occurred
    * 
    * @param creatorInfo
    * @param lifetime
    * @param topicInfo
    * @param synopsisType
    * @param topicSynopsis
    */
   public boolean createTopic(String creatorInfo, Calendar lifetime,
            String topicInfo, int synopsisType, String topicSynopsis,
            Vector authorizedUsers, int timeout) {

      long start = hrClock.getTimeMicroseconds();

      TopicCreationRequest topicCreationReq = new TopicCreationRequest();
      topicCreationReq.setEntityId(this.EntityID);
      topicCreationReq.setCreatorInfo(creatorInfo);
      topicCreationReq.setLifetime(lifetime);
      topicCreationReq.setTopicInfo(topicInfo);
      topicCreationReq.setSynopsisType(synopsisType);
      topicCreationReq.setTopicSynopsis(topicSynopsis);
      topicCreationReq.setCertificate(Credentials);

      // HG: Added Aug 15, 2006
      // Prevent unauthorized discovery
      if (authorizedUsers != null) {
         for (int i = 0; i < authorizedUsers.size(); i++)
            topicCreationReq.setAllowedToDiscoverBy((String) authorizedUsers
                     .elementAt(i));
      }

      if (selectTDN()) {
         // Null entry for this topic
         _myTopicAds.put(topicSynopsis, NEW_TOPIC_PLACEHOLDER);

         // Encrpt TOPIC Creation Request using TDN's public key
         Certificate tdnCert = (Certificate) _TDNList.get(_selectedTDN);

         EncryptedMessagePayload emp = EncryptedMessagePayload.generate(
                  topicCreationReq.getBytes(), tdnCert.getPublicKey());
         if (emp == null) { return false; }

         send(Topics.TDN_TOPIC_BASE + "/" + _selectedTDN, emp.getBytes());

         topicCreationLock.lock(timeout);

         if (BENCHMARK) {
            long stop = hrClock.getTimeMicroseconds();
            long time = stop - start;
            try {
               fout.write(("BENCHMARK # TopicCreation:" + time + "\n")
                        .getBytes());
            } catch (IOException e2) {
               log.error("", e2);
            }
         }

         // Check if topic creation response has arrived...
         if (_myTopicAds.get(topicSynopsis).equals(NEW_TOPIC_PLACEHOLDER)) {
            _myTopicAds.remove(topicSynopsis);
            return false;
         }
         return true;
      } else
         return false;
   }

   /**
    * Returns the topic's UUID corresponding to the specified topic synopsis
    * 
    * @param topicSynopsis
    * @return UUID of the specified topic, NULL if the topic sysnopsis was not
    *         found
    */
   public String getTopicUUID(String topicSynopsis) {
      SignedTopicAdvertisement signedTopicAd = (SignedTopicAdvertisement) _myTopicAds
               .get(topicSynopsis);
      if (signedTopicAd == null)
         return null;
      else
         return signedTopicAd.getTopicAd().getUuid();
   }

   /**
    * Returns the signed topic advertisement corresponding to the specified
    * topic synopsis
    * 
    * @param topicSynopsis
    * @return Signed topic advertisement of the specified topic, NULL if the
    *         topic sysnopsis was not found
    */
   public SignedTopicAdvertisement getSignedTopicAdvertisement(
            String topicSynopsis) {
      return (SignedTopicAdvertisement) _myTopicAds.get(topicSynopsis);
   }

   /**
    * Selects a TDN from the list of available TDN's. Currently this simply
    * selects the first TDN. A better policy needs to be implemented
    * 
    * @return TRUE if a TDN was selected, FALSE otherwise
    */
   public boolean selectTDN() {
      if (_TDNList.size() == 0) return false;

      for (Iterator iter = _TDNList.keySet().iterator(); iter.hasNext();) {
         String uuid = (String) iter.next();

         // TODO: Perhaps check TDN Credentials, PING for liveness or some
         // other metric for selecting a TDN

         // Currrently just select the first TDN in the hashtable
         _selectedTDN = uuid;
         return true;
      }
      _selectedTDN = null;
      return false;
   }

   /**
    * Publishes the specified request(bytes) on the specifed topic
    * 
    * @param topicName -
    *           The topic to which the request must be published
    * @param request -
    *           The request to be published (in bytes)
    */
   public void send(String topicName, byte[] request) {
      producer.generateEventIdentifier(true);
      producer.setTemplateId(12345);
      producer.setDisableTimestamp(false);
      try {
         NBEvent nbEvent = producer.generateEvent(
                  TemplateProfileAndSynopsisTypes.STRING, topicName, request);
         producer.publishEvent(nbEvent);
      } catch (ServiceException e) {
         log.error("", e);
      }
   }

   /**
    * Process recieved events
    * 
    * @see cgl.narada.service.client.NBEventListener#onEvent(cgl.narada.event.NBEvent)
    */
   public void onEvent(NBEvent nbEvent) {
      String topic = (String) nbEvent.getContentSynopsis();

      if (topic.equals(Entity_TOPIC)) {
         // What type of message was sent

         EncryptedMessagePayload emp = EncryptedMessagePayload
                  .createObjectFromBytes(nbEvent.getContentPayload());

         byte[] payload = emp.getDecryptedPayload(privateKey);

         if (payload == null) {
            log.error("Unable to decrypt Payload: ");
            return; // DISCARD, If error occurs
         }

         byte eventType = payload[0];
         byte[] obj = new byte[payload.length - 1];
         System.arraycopy(payload, 1, obj, 0, obj.length);

         if (eventType == EventPayloadHeader.EVENT_TDN_DISCOVERY_RESPONSE) {
            // Process TDN Discovery
            TDNDiscoveryResponse discoResp = TDNDiscoveryResponse
                     .createObjectFromBytes(obj);

            if (discoResp == null) {
               log
                        .warn("{EVENT_TDN_DISCOVERY_RESPONSE}Garbage EVENT recieved !");
               return;
            }

            String tdnKey = discoResp.getTDN_ID();

            log.info("Found TDN: " + tdnKey);

            // Verify credentials before storing them
            log.info("Verifying TDN's Certificate...");

            Certificate cert = discoResp.getCredentials();

            try {
               cert.verify(rootPublicKey);
            } catch (Exception e) {
               log.error("Unable to verify TDN certificate", e);

               // Discard certificate and also the response
               return;
            }
            log.info("TDN's Certificate Verified !");
            // Store the key of TDN and the TDN credentials for future use
            // New credentials replace previous credentials if any...
            // Credentials are verified before storing
            _TDNList.put(tdnKey, cert);

            tdnDiscoLock.unlock();

         } else if (eventType == EventPayloadHeader.EVENT_TOPIC_CREATION_RESPONSE) {
            // Process TOPIC Ad
            System.out
                     .println("Entity.onEvent(): EVENT_TOPIC_CREATION_RESPONSE");

            TopicAdvertisement topicAd = TopicAdvertisement
                     .createObjectFromBytes(obj);

            if (topicAd == null) {
               log
                        .warn("{EVENT_TOPIC_CREATION_RESPONSE}Garbage EVENT recieved !");
               return;
            }

            // Content synopsis is the key
            String key = topicAd.getTopicSynopsis();

            // Strip out the UUID part
            key = key.substring(key.indexOf("::") + 2);

            if (_myTopicAds.containsKey(key)) {
               if (_myTopicAds.get(key).equals(NEW_TOPIC_PLACEHOLDER)) {

                  SignedTopicAdvertisement sta = new SignedTopicAdvertisement(
                           topicAd, Credentials);
                  if (sta.generateSignedDigest(privateKey)) {

                     // Store my topic Ad...
                     _myTopicAds.put(key, sta);

                     // and also publish it for other TDNs
                     send(Topics.TOPIC_ADS, sta.getBytes());
                  }
                  // Finally wakeup waiting threads if any...
                  topicCreationLock.unlock();
               } // Else its a response *NOT* meeting timing
               // constraints...
               // should try some other TDN if possible
            } else {
               log.warn("Unable to find matching topic creation "
                        + "request placeholder for the Topic Ad !");
            }
         }
      }
   }

   public static void main(String[] args) {

      if (args[0].equals("-help")) {
         System.out.println("USAGE: ");
         System.out
                  .println("ARG_0: <hostnameToUse> OR -auto (for using broker discovery");
         System.out.println("ARG_1: <port>");
         System.out.println("ARG_2: <protocol>");
         System.out.println("-----------------------");
         System.exit(0);
      }

      SystemInit.init();

      String host = "localhost";
      String port = "25000";
      String protocol = "niotcp";
      host = args[0];
      if (args.length > 1) port = args[1];
      if (args.length > 2) protocol = args[2];

      int clientEntityId = 55; // Integer.parseInt(args[0]);
      String alias = "c1";

      // MUST INITIALIZE THE CertificateManager
      CertificateManager certMan = new CertificateManager();

      String nbHome = null;
      try {
         nbHome = SystemInit.getInitData(SystemInit.SYS_NB_HOME);
      } catch (SystemNotInitializedException e2) {
         log.error("", e2);
      }

      certMan.init(nbHome + "/keystore/NBSecurityTest.keys", null);

      Certificate tdnCert = CertificateUtil.getCertificate(certMan, alias);
      PrivateKey privkey = CertificateUtil.getPrivateKey(certMan, alias);
      PublicKey rootCAPub = CertificateUtil.getPublicKey(certMan,
               certMan.ROOT_CA_ALIAS);

      // Should use broker discovery when its in place...
      // For now just use some broker and port...

      Entity e = new Entity(clientEntityId, nbHome
               + "/config/ServiceConfiguration.txt", alias, tdnCert, privkey,
               rootCAPub, host, port, protocol);

      // BENCHMARK
      if (BENCHMARK) {
         try {
            fout = new FileOutputStream(args[3]);
         } catch (FileNotFoundException e1) {
            log.error("", e1);
         }
      }
      // for (int i = 0; i < 90; i++) {
      //
      // System.out.println("RUN: " + i);
      //
      // long start = e.hrClock.getTimeMicroseconds();

      if (e.sendTDNDiscoveryRequest(10000)) {
         System.out.println("Found TDN ! Proceeding to create TOPIC !!");

         Calendar until = Calendar.getInstance(TimeZone.getTimeZone("GMT-5"));
         until.set(2007, Calendar.MAY, 13, 5, 30, 0);

         Vector v = new Vector();
         v.add("CN=client2, OU=CGL, O=IU, L=Bloomington, C=US");
         
         boolean result = e
                  .createTopic("HG", until, "MyTopic1",
                           TemplateProfileAndSynopsisTypes.STRING, "Topic1",
                           v, 10000);
         if (result) {
            System.out.println("Topic Creation Successful !");
            System.out.println("TOPIC UUID: " + e.getTopicUUID("Topic1"));
         } else {
            System.err
                     .println("TOPIC Creation Unsuccessful, Perhaps a timeout occurred OR TDN was unreachable !");
         }
      } else
         System.err.println("NO TDN found within specified timeout period !!");

      // BECNHMARK
      // long stop = e.hrClock.getTimeMicroseconds();
      // long time = stop - start;
      // System.out.println("TIME: " + time + " microSec");
      // try {
      // fout.write(("TIME # TOTAL:" + time + "\n").getBytes());
      // } catch (IOException e2) {
      // log.error("", e2);
      // }
      //
      // }
      System.exit(0);

   }
}